package wortubung;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Map;

import javax.script.*;

import wortubung.core.LANG;
import wortubung.inflection.InflectionDisplayFormat;
import wortubung.lang.LanguageManager;
import wortubung.lang.WordCollection;
import wortubung.quiz.QuizGenOptions;
import wortubung.quiz.QuizManager;
import wortubung.util.ImmutableStringList;
import wortubung.util.ToolBase;
import wortubung.util.TransliterateException;
import wortubung.wordlist.WordListInfo;

import static wortubung.util.CommonUtils.*;
import static wortubung.util.MatrixPrinter.*;
import static wortubung.util.SgmlEntity.*;

public class WortUbung extends ToolBase
{
	private static final String INIT_FILE = "wu.init";
	private static final int MIN_PAGE_WIDTH = 60;

	private static final String header = "\n" +
        " ***************************************************************************\n" +
        " **  Wortübung - A shell to aid language study, primarily for vocabulary  **\n" +
        " **              but also for idioms, structures, grammatic rules, etc.   **\n" +
        " **                                                                       **\n" +
        " **  Created by James J.B. Huang, incepted in 2014                        **\n" +
        " ***************************************************************************\n";

    public static void main(String[] args) {
        WortUbung wortUbung = new WortUbung();
        wortUbung.origArgs = args;
        try {
        	try {
                wortUbung.init(args);
                if (wortUbung.isHelp)
                	return;
                wortUbung.run();
            } catch(Exception e) {
            	wortUbung.error(e);
            }
        } finally {
        	try {
        		wortUbung.closeOutputFile();
        	} catch(Exception e) {
        		log.println(e.getMessage());
        	}
        }
    }

    public static final int DEFAULT_PAGE_WIDTH = 100;
    public static final String PROMPT = "wortübung";
    public static final String DEFAULT_PIPE = "wu.log";

    public static BufferedReader in;
    public static InflectionDisplayFormat format = InflectionDisplayFormat.LINE;

    static {
    	try {
    		in = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
    	} catch(Exception e) {
    		e.printStackTrace();
    	}
    }

	String[] origArgs;
	File pipeFile;
	PrintWriter pipeWriter;
	boolean verbose = false;
	String cmd = null;
	boolean isJS = false;
	String script = null;
	String[] jsArgs = null;
	QuizManager _quizGen = null;
	String editorExec = "notepad";

	protected LanguageManager[] langManagers;
	protected LanguageManager curLM;

	public String getEditorExec() {
		return editorExec;
	}

	public LanguageManager getLangManager(LANG lang) {
		for (LanguageManager lm : langManagers) {
			if (lm.getLanguage() == lang)
				return lm;
		}
		return null;
	}

	public LanguageManager getCurrentLanguageManager() { return curLM; }

	private WortUbung() {
    	langManagers = LanguageManager.getAllManagers(this);
	}

	private void run() throws Exception {
		if (isEmpty(script)) {
			if (isNotEmpty(cmd))
				runCommand(cmd, true);
			else
				interact();
		} else {
			if (isJS)
				runJS(script);
			else
				runScript(script, false);
			if (isNotEmpty(cmd))
				runCommand(cmd, true);
		}
	}

	private void runScript(String scriptFile, boolean quite) {
		LineNumberReader lnr = null;
		try {
			lnr = openFile(new File(scriptFile));
			String line;
			while ((line = lnr.readLine()) != null) {
				if (runCommand(line, true))
					break;
			}
		} catch(Exception e) {
			if (!quite)
				log.println(e.getMessage());
		} finally {
			try { lnr.close(); } catch(Exception ee) {}
		}
	}

	private void runJS(String scriptFile) throws Exception {
		String jsProg = readFileAsString(new File(scriptFile));
		
		String[] engines = { "nashorn", "JavaScript" }; // java8 or java7
		ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = null;
		for (String e : engines) {
        	if ((engine = factory.getEngineByName(e)) != null)
        		break;
		}
		if (engine == null) {
			error("JavaScript engine is not available");
			return;
		}
        engine.put("wortubung", this);
		if (jsArgs != null)
			engine.put("args", jsArgs);
		engine.eval(jsProg);
	}

	private String getPrompt() {
		return PROMPT + '_' + curLM.getLanguage().name() + "# ";
	}

	private void interact() {
    	promptln(header);
    	LanguageManager.showStatsGeneral();
    	runScript(INIT_FILE, true);
    	curLM.loadLatest();

    	while (true) {
    		prompt(getPrompt());
    		String line;
    		try {
    			line = in.readLine().trim();
    		} catch(Exception e) {
    			if (verbose)
    				error(e);
    			break;
    		}
   			try {
   	   			if (runCommand(line, false))
   	   				break;
   			} catch(WortUbungException wbe) {
   				promptln(wbe.getMessage());
			}
    	}
    	
    	curLM.saveLatest();
    }

	/**
	 * Public interface for scripts to call.
	 */
    public void runCommand(String cmdline) {
    	runCommand(cmdline, true);
    }

	/**
	 * @return true to terminate
	 */
    protected boolean runCommand(String cmdline, boolean inScript) {
    	if (isEmpty(cmdline) || cmdline.trim().startsWith("#"))
    		return false;

    	setEchoing(!inScript);
    	String[] cmds;
    	if (cmdline.startsWith("/cb ") || cmdline.startsWith("/clipboard "))
    		cmds = new String[]{ cmdline };
    	else
    		cmds = cmdline.split(";");
    	for (String c : cmds) {
        	Object[] cmdInfo = WortUbungCommand.checkCommand(c.trim());
    		String params = (String)cmdInfo[0];
    		WortUbungCommand cmd = (WortUbungCommand)cmdInfo[1];
    		if (cmd.isPiped)
    			initPipe(c);
    		try {
        		switch(cmd) {

        		// finding and showing words
        		default:               curLM.findAndShowWords(c); break;
        		case CMD_DEF:          curLM.findAndShowWordsByDescription(params); break;
        		case CMD_LATEST:       showLatest(params); break;
        		
        		// studying
        		case CMD_DAILY:        PersonalStudy.doDaily(params, this); break;
        		case CMD_SHOW:         doShowConstants(params); break;

        		// inflection
        		case CMD_CONJUG:       curLM.conjugate(params, format, false); break;
        		case CMD_CONJUG_X:     curLM.conjugate(params, format, true); break;
        		case CMD_UNCONJUG:     curLM.unconjugate(params); break;
        		case CMD_DECLINE:      curLM.decline(params); break;
        		case CMD_COMPARATIVE:  curLM.comparative(params); break;
        		case CMD_SPECIAL_CONJ: curLM.listSpecialConjugations(); break;

        		// word list handling
        		case CMD_LANG:         doLang(params); break;
        		case CMD_LIST:         doList(params); break;
        		case CMD_LIST_INFO:    curLM.showListInfo(params); break;
        		case CMD_LIST_EDIT:    editLists(params); break;
        		case CMD_LIST_PREFIX:  curLM.showListPrefix(params); break;
        		case CMD_PREFIX_COUNT: curLM.showPrefixCounts(params, false); break;

        		// testing and printing
        		case CMD_PRINT:        curLM.doPrintWords(params); break;
        		case CMD_FILL_BLANK:   curLM.showPrefixCounts(params, true); break;
        		case CMD_QUIZ:         doOrPrintQuiz(params, false); break;
        		case CMD_GEN_QUIZ:     doOrPrintQuiz(params, true); break;
        		case CMD_SCORE_QUIZ:   getQuizManager().scoreQuizes(params); break;

        		// system settings
        		case CMD_QUIT:         return doQuit();
        		case CMD_HELP:         doHelp(params); break;
        		case CMD_STATS:        LanguageManager.showStatsGeneral(); break;
        		case CMD_PAGE_WIDTH:   doPageWidth(params); break;
        		case CMD_PIPE:         doPipe(params); break;
        		case CMD_RELOAD:       doReload(null); break;
        		case CMD_CLIPBOARD:    doClipboard(params); break;
        		case CMD_EDITOR:
        			if (isEmpty(params))
        				promptln("Current editor: " + editorExec);
        			else
        				promptln("Editor executable is set to '" + (editorExec = params) + "'.");
        			break;
        		case CMD_VERBOSE:
        			if (isEmpty(params))
        				promptln("verbose: " + onOff(verbose));
        			else
        				verbose = !"off".equalsIgnoreCase(params);
        			break;

        		// internal
        		case _INSPECT:
        			curLM.doInspectWordsInLists(params);
        			break;
        		case _FORMAT:
        			if (isEmpty(params))
        				promptln("Current format: " + format);
        			else
        				promptln("Format is set to '" + (format = InflectionDisplayFormat.valueOf(params.toUpperCase())) + "'.");
        			break;
        		case _DEBUG:
       				setDebugOut("off".equalsIgnoreCase(params) ? null : this);
        			break;
        		case _UNKNOWN:
        			error("Invalid command.\n");
        			doHelp(null);
        			break;
        		}
        	} finally {
        		closePipe();
        	}
    	}
    	return false;
    }

    private boolean doQuit() {
    	try {
    		PrintWriter fw = openFileToWrite(new File(INIT_FILE), false);
    		fw.println("/pw " + getPageWidth());
    		fw.println("/verbose " + onOff(verbose));
    		fw.println("/lang " + curLM.getLanguage().name());
    		fw.close();
    	} catch(Exception e) {
    		error(e);
    	}
    	return true;
    }

    private void doHelp(String params) {
    	if (eq(params, "input", false))
    		InputMethodHelp.showHelp(this);
    	else
    		WortUbungCommand.showHelp(this, params);
    }

    private void doPageWidth(String params) {
    	try {
    		int width = Integer.parseInt(params);
    		if (width < MIN_PAGE_WIDTH) {
    			curLM.promptln(Messages.PAGE_WIDTH_TOO_SMALL, width, MIN_PAGE_WIDTH);
    		} else if (width != getPageWidth()) {
    			curLM.promptln(Messages.SET_PAGE_WIDTH_TO, width, getPageWidth());
    			setPageWidth(width);
    		}
    	} catch(Exception e) {
    		promptln("Page width: " + getPageWidth() + '\n');
    	}
    }

    private void doPipe(String params) {
    	if ("off".equalsIgnoreCase(params)) {
    		if (pipeFile != null)
    			error("Piping into '", pipeFile, "' is now turned off.");
    		else
    			error("No piping set up yet.");
    		pipeFile = null;
    		return;
    	}
    	boolean isClear = "clear".equalsIgnoreCase(params);

    	try {
    		File f = new File(neverEmpty(params, DEFAULT_PIPE));
    		if (pipeFile == null) {
    	    	if (isClear) {
	   				error("To clear a pipe file, it must be enabled first.");
	   				return;
    	    	}
    			pipeFile = f;
    		} else if (isClear) {
				if (pipeFile.exists() && pipeFile.isFile())
					pipeFile.delete();
    		} else if (f.getAbsolutePath().equals(pipeFile.getAbsolutePath())) {
				promptln("Pipe file ", params, " is already set to ", params, ". No change.");
				return;
			} else {
				promptln("Previous pipe file: " + pipeFile);
				pipeFile = f;
    		}
    		if (pipeFile.getParentFile() != null)
    			pipeFile.getParentFile().mkdirs();
    		new FileOutputStream(pipeFile, true).close();
    		promptln("Current pipe file: " + pipeFile);
    	} catch(Exception e) {
    		error(e);
    	}
    }
    
	private void doShowConstants(String params) {
		Map<LANG,ImmutableStringList> words = null;
		if ("seasons".equalsIgnoreCase(params))
			words = GeneralStudy.getSeasonNames();
		else if ("weekdays".equalsIgnoreCase(params))
			words = GeneralStudy.getWeekdayNames();
		else if ("months".equalsIgnoreCase(params))
			words = GeneralStudy.getMonthNames();
		else
			return;
		
		LanguageManager[] lms = LanguageManager.getAllManagers(this);
		String[] titles = new String[lms.length];
		int i, j;
		int cnt = words.get(LANG.en).size();
		String[][] content = new String[cnt][];
		for (i=0; i<cnt; ++i)
			content[i] = new String[titles.length];

		for (j=0; j<titles.length; ++j) {
			titles[j] = lms[j].getLangName();
			ImmutableStringList col = words.get(lms[j].getLanguage());
			if (col != null) {
				for (i=0; i<cnt; ++i)
					content[i][j] = col.get(i);
			}
		}
		printMatrix(this, true, titles, content, false, false);
	}

    private void doLang(String params) {
    	if (isEmpty(params)) {
    		String msg = "";
    		for (LanguageManager lm : langManagers)
    			msg += "  " + lm.getLanguage().getName(curLM.getLanguage());
    		curLM.promptln(Messages.SUPPORTED_LANGUAGES, msg);
    		return;
    	}

    	LANG lang = LANG.guess(params);
    	if (lang == null) {
    		error("Unrecognized language '" + params + "'.");
    		return;
    	}
    	LanguageManager lm = getLangManager(lang);
    	if (lm == null) {
    		error(lang.getLongName() + " language is not yet supported.");
    		return;
    	}
    	curLM = lm;
    	curLM.loadLatest();
		promptln("Current language: " + curLM.getLanguage().getLongName() + '.');
		promptln();
    }

    private void doList(String params) {
    	if (isEmpty(params))
    		curLM.promptln(Messages._LIST_TAKES);
    	else
    		new WordCollection(curLM).findAndShowWordLists(params);
    }
    
    private void doClipboard(String params) {
    	params = params.trim();
    	String txt;
    	try {
	    	if (!"\\".equals(params.trim())) {
					txt = curLM.toVernacular(params);
	    	} else {
	    		StringBuilder sb = new StringBuilder();
	    		try {
	    			while (true) {
	        			txt = in.readLine();
	        			if (txt.trim().equals("."))
	        				break;
	        			if (sb.length() > 0)
	        				sb.append('\n');
	        			sb.append(curLM.toVernacular(txt.trim()));
	    			}
	    			txt = sb.toString();
	    		} catch(IOException ioe) {
	        		error(ioe);
			    	return;
	    		}
	    	}
	    	if (curLM.getLanguage() == LANG.fr)
	    		txt = txt.replace("SVP", "s'il vous plaît");
	    	setToClipboard(unescapeSgmlEntity(txt));
    	} catch (TransliterateException e) {
    		promptln(e.getMessage());
    	}
    }

    private void doOrPrintQuiz(String params, boolean printing) {
    	QuizGenOptions options = new QuizGenOptions(this, params, !printing);
    	if (options.isHelp())
   			options.showHelp(null);
    	else
    		getQuizManager().doOrPrintQuiz(options, printing ? null : in);
    }

    private void showLatest(String params) {
    	boolean sort = false;
    	boolean simple = false;
    	if (isNotEmpty(params)) {
        	for (String p : params.split("-")) {
        		p = p.trim();
        		if ("sort".equals(p))
        			sort = true;
        		else if ("simple".equals(p))
        			simple = true;
        	}
    	}
    	curLM.showLatest(sort, simple);
    }

    private void editLists(String params) {
		if (isEmpty(params)) {
			curLM.promptln(Messages.SPECIFY_LIST_SHORT_NAME);
			return;
		}
		curLM.editLists(params.split(" +"));
    }

	private QuizManager getQuizManager() {
    	if (_quizGen == null)
    		_quizGen = new QuizManager(this);
    	return _quizGen;
	}

    private void initPipe(String line) {
		if (pipeFile != null) {
			try {
    			pipeWriter = openFileToWrite(pipeFile, true);
    			if (verbose && line != null)
    				pipeWriter.println(getPrompt() + line);
			} catch(Exception e) {
	    		error(e);
			}
		}
    }

    private void closePipe() {
    	if (pipeWriter != null) {
    		try { pipeWriter.close(); } catch(Exception e) {}
    		pipeWriter = null;
    	}
    }

    @Override
    protected void init(String[] args) throws IOException {
    	setPageWidth(DEFAULT_PAGE_WIDTH);

    	doReload(args);

    	for (LanguageManager lm : langManagers) {
    		if (lm.getWords().size() > 0) {
    			curLM = lm;
    			break;
    		}
    	}
    	if (curLM == null)
    		curLM = langManagers[0];
    }

    private void doReload(String[] args) {
    	try {
    		super.init(args == null ? origArgs : args);

    		if (args == null) {
	    		PersonalStudy.reset();
	    		for (LanguageManager lm : langManagers)
    				lm.reset();
    		}

    		for (String fname : inFilenames)
               	readDataFile(new File(fname));
    		PersonalStudy.finish();

    	} catch(Exception e) {
    		error(e);
    	}
    }

    private void readDataFile(File file) throws IOException {
    	if (file.isDirectory()) {
    		for (String fn : file.list())
   				readDataFile(new File(file, fn));
    	} else {
    		String fn = file.getName();
    		if (fn.endsWith(".wdl"))
    			readWordFile(file);
    		else if (fn.endsWith(".study"))
    			readStudyFile(file);
    		else
    			promptln("Unknown file extension: " + fn);
    	}
    }

    private void readWordFile(File file) throws IOException {
		LineNumberReader lnr = openFile(file);
        try {
            WordListInfo wordListInfo = LanguageManager.parseWordListMetaLine(file, lnr);
            LanguageManager lm = getLangManager(wordListInfo.getLang());
            if (lm != null)
            	lm.parseLineFile(wordListInfo, lnr);
        } catch(WortUbungException wue) {
        	wue.setFileInfo(file.getName(), lnr.getLineNumber()-1);
        	log.println("Error: " + wue.getMessage());
        } finally {
            lnr.close();
        }
    }

    private void readStudyFile(File file) throws IOException {
		LineNumberReader lnr = openFile(file);
        try {
            String line = lnr.readLine(); // first line
            if (line != null)
            	line = line.trim();
            int idx = line.indexOf("#");
            if (idx < 0)
                throw new WortUbungException("Study file is not valid: no meta information found.");

            String month = null;
    		for (String s : line.substring(idx+1).split(",")) {
    			String[] sa = s.split("=");
    			if (sa.length == 2) {
    				s = sa[0].trim();
    				String v = sa[1].trim();
    				if ("month".equals(s))
    					month = v;
    			}
    		}
            PersonalStudy.addCollection(month, file);
        } finally {
            lnr.close();
        }
    }

    @Override
    protected String[] getCmdlineOptions() {
        final String[] options = {
        		"-script=script_file.wu",
        		"-js=\"js_file.js [arguments]\"",
        		"-cmd=commnad" };
        return concat(super.getCmdlineOptions(), options);
    }

    @Override
    protected void cmdlineOption(String option, String value) {
        if ("cmd".equalsIgnoreCase(option) && isNotEmpty(value))
        	cmd = value;
        else if ("script".equalsIgnoreCase(option) && isNotEmpty(value)) {
        	script = value;
        	isJS = false;
        }
        else if ("js".equalsIgnoreCase(option) && isNotEmpty(value)) {
        	String[] sa = parseArguments(value);
        	script = sa[0];
        	jsArgs = (sa.length <= 1) ? null : Arrays.copyOfRange(sa, 1, sa.length);
        	isJS = true;
        }
        else
            super.cmdlineOption(option, value);
    }

    @Override
    protected void printHelpText() {
    	log.println(
    			"\nWith -script=, the commands in the script are executed.\n" +
 	    	    "With -js=\"js_file [arguments]\", the javascript program is run and returns.\n" +
    			"    The program name and arguments can be quoted in single-quotes.\n" +
    	    	"With -cmd=, the command is executed (following the script if specified) and returns.\n" +
    	        "If no script nor command specified, it enters the interactive mode.\n\n" +

    	        "JavaScript programming:\n" +
    	        "    A 'wortubung' property is preset with these methods:\n" +
    	        "        wortubung.runCommand(command);\n" +
    	        "        wortubung.getListShortNames(); // returns a String[]\n" +
    	        "    The 'args' property holds the arguments if specified, otherwise it is undefined.\n\n" +

    	        "When entering the interactive mode, the shell first reads from wu.init file, if present.\n" +
    	        "On exit, the current settings are persisted to wu.init.");
    }

    @Override
    public void print(String msg) {
    	if (!isPrinting() && pipeWriter != null)
   			pipeWriter.print(msg);
    	super.print(msg);
    }
    
    @Override
    public void println() {
    	if (!isPrinting() && pipeWriter != null)
   			pipeWriter.println();
    	super.println();
    }
}
